﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nes4Net
{
    public class Mappers
    {
        public static Mapper0 GetMapper(Int32 index, Nes nes)
        {
            return index switch
            {
                0 => new Mapper0(nes),
                1 => new Mapper1(nes),
                2 => new Mapper2(nes),
                3 => new Mapper3(nes),
                4 => new Mapper4(nes),
                5 => new Mapper5(nes),
                7 => new Mapper7(nes),
                11 => new Mapper11(nes),
                34 => new Mapper34(nes),
                38 => new Mapper38(nes),
                66 => new Mapper66(nes),
                94 => new Mapper94(nes),
                140 => new Mapper140(nes),
                180 => new Mapper180(nes),
                _ => null,
            };
        }

    }

    public class Mapper0
    {
        public Nes Nes { get; set; }

        private Int32 joy1StrobeState;
        private Int32 joy2StrobeState;
        private Int32 joypadLastWrite;
        public Boolean zapperFired;
        public Int32? zapperX;
        public Int32? zapperY;

        public Mapper0(Nes nes)
        {
            this.Nes = nes;
        }

        public virtual void Reset() 
        {
            this.joy1StrobeState = 0;
            this.joy2StrobeState = 0;
            this.joypadLastWrite = 0;

            this.zapperFired = false;
            this.zapperX = null;
            this.zapperY = null;
        }

        public virtual void Write(Int32 address, Int32 value)
        {
            if (address < 0x2000)
            {
                // Mirroring of RAM:
                this.Nes.Cpu.Mem[address & 0x7ff] = value;
            }
            else if (address > 0x4017)
            {
                this.Nes.Cpu.Mem[address] = value;
                if (address >= 0x6000 && address < 0x8000)
                {
                    // Write to persistent RAM
                    this.Nes.Opts.OnBatteryRamWrite(address, value);
                }
            }
            else if (address > 0x2007 && address < 0x4000)
            {
                this.RegWrite(0x2000 + (address & 0x7), value);
            }
            else
            {
                this.RegWrite(address, value);
            }
        }

        public void Writelow(Int32 address, Int32 value)
        {
            if (address < 0x2000)
            {
                // Mirroring of RAM:
                this.Nes.Cpu.Mem[address & 0x7ff] = value;
            }
            else if (address > 0x4017)
            {
                this.Nes.Cpu.Mem[address] = value;
            }
            else if (address > 0x2007 && address < 0x4000)
            {
                this.RegWrite(0x2000 + (address & 0x7), value);
            }
            else
            {
                this.RegWrite(address, value);
            }
        }

        public virtual Int32 Load (Int32 address)
        {
            // Wrap around:
            address &= 0xffff;

            // Check address range:
            return (address) switch
            {
                // ROM:
                > 0x4017 => this.Nes.Cpu.Mem[address],
                // I/O Ports.
                >= 0x2000 => this.RegLoad(address),
                // RAM (mirrored)
                _ => this.Nes.Cpu.Mem[address & 0x7ff]
            };

            //// Check address range:
            //if (address > 0x4017)
            //{
            //    // ROM:
            //    return this.Nes.Cpu.Mem[address];
            //}
            //else if (address >= 0x2000)
            //{
            //    // I/O Ports.
            //    return this.RegLoad(address);
            //}
            //else
            //{
            //    // RAM (mirrored)
            //    return this.Nes.Cpu.Mem[address & 0x7ff];
            //}
        }

        internal Int32 RegLoad(Int32 address)
        {
            switch (
              address >> 12 // use fourth nibble (0xF000)
            )
            {
                case 0:
                    break;
                case 1:
                    break;
                case 2:
                // Fall through to case 3
                case 3:
                    // PPU Registers
                    switch (address & 0x7)
                    {
                        case 0x0:
                            // 0x2000:
                            // PPU Control Register 1.
                            // (the value is stored both
                            // in main memory and in the
                            // PPU as flags):
                            // (not in the real NES)
                            return this.Nes.Cpu.Mem[0x2000];
                        case 0x1:
                            // 0x2001:
                            // PPU Control Register 2.
                            // (the value is stored both
                            // in main memory and in the
                            // PPU as flags):
                            // (not in the real NES)
                            return this.Nes.Cpu.Mem[0x2001];
                        case 0x2:
                            // 0x2002:
                            // PPU Status Register.
                            // The value is stored in
                            // main memory in addition
                            // to as flags in the PPU.
                            // (not in the real NES)
                            return this.Nes.Ppu.readStatusRegister();
                        case 0x4:
                            // 0x2004:
                            // Sprite Memory read.
                            return this.Nes.Ppu.sramLoad();
                        case 0x3:
                            return 0;
                        case 0x5:
                            return 0;
                        case 0x6:
                            return 0;
                        case 0x7:
                            // 0x2007:
                            // VRAM read:
                            return this.Nes.Ppu.vramLoad();
                    }
                    break;
                case 4:
                    // Sound+Joypad registers
                    switch (address - 0x4015)
                    {
                        case 0:
                            // 0x4015:
                            // Sound channel enable, DMC Status
                            return this.Nes.Apu.ReadReg(address);

                        case 1:
                            // 0x4016:
                            // Joystick 1 + Strobe
                            return this.Joy1Read();

                        case 2:
                            // 0x4017:
                            // Joystick 2 + Strobe
                            // https://wiki.nesdev.com/w/index.php/Zapper
                            Int32 w;

                            if (
                              this.zapperX != null &&
                              this.zapperY != null &&
                              this.Nes.Ppu.isPixelWhite(this.zapperX.Value, this.zapperY.Value)
                            )
                            {
                                w = 0;
                            }
                            else
                            {
                                w = 0x1 << 3;
                                //w = 0x1 << 3;
                            }

                            if (this.zapperFired)
                            {
                                w |= 0x1 << 4;
                            }
                            return (this.Joy2Read() | w) & 0xffff;
                    }
                    break;
            }
            return 0;
        }

        public void RegWrite(Int32 address, Int32 value)
        {
            switch (address)
            {
                case 0x2000:
                    // PPU Control register 1
                    this.Nes.Cpu.Mem[address] = value;
                    this.Nes.Ppu.updateControlReg1(value);
                    break;

                case 0x2001:
                    // PPU Control register 2
                    this.Nes.Cpu.Mem[address] = value;
                    this.Nes.Ppu.updateControlReg2(value);
                    break;

                case 0x2003:
                    // Set Sprite RAM address:
                    this.Nes.Ppu.writeSRAMAddress(value);
                    break;

                case 0x2004:
                    // Write to Sprite RAM:
                    this.Nes.Ppu.sramWrite(value);
                    break;

                case 0x2005:
                    // Screen Scroll offsets:
                    this.Nes.Ppu.scrollWrite(value);
                    break;

                case 0x2006:
                    // Set VRAM address:
                    this.Nes.Ppu.writeVRAMAddress(value);
                    break;

                case 0x2007:
                    // Write to VRAM:
                    this.Nes.Ppu.vramWrite(value);
                    break;

                case 0x4014:
                    // Sprite Memory DMA Access
                    this.Nes.Ppu.sramDMA(value);
                    break;

                case 0x4015:
                    // Sound Channel Switch, DMC Status
                    this.Nes.Apu.WriteReg(address, value);
                    break;

                case 0x4016:
                    // Joystick 1 + Strobe
                    if ((value & 1) == 0 && (this.joypadLastWrite & 1) == 1)
                    {
                        this.joy1StrobeState = 0;
                        this.joy2StrobeState = 0;
                    }
                    this.joypadLastWrite = value;
                    break;

                case 0x4017:
                    // Sound channel frame sequencer:
                    this.Nes.Apu.WriteReg(address, value);
                    break;

                default:
                    // Sound registers
                    // console.log("write to sound reg");
                    if (address >= 0x4000 && address <= 0x4017)
                    {
                        this.Nes.Apu.WriteReg(address, value);
                    }
                    break;
            }
        }

        public Int32 Joy1Read ()
        {
            var ret = this.joy1StrobeState switch
            {
                > -1 and < 8  => this.Nes.Controllers[0].state[this.joy1StrobeState],
                // < 19 => 0,
                19 => 1,
                _ => 0,
            };
            this.joy1StrobeState++;
            if (this.joy1StrobeState == 24)
            {
                this.joy1StrobeState = 0;
            }

            return ret;
        }

        public Int32 Joy2Read ()
        {
            var ret = this.joy2StrobeState switch
            {
                > -1 and < 8 => this.Nes.Controllers[1].state[this.joy2StrobeState],
                // < 19 => 0,
                19 => 1,
                _ => 0,
            };
            this.joy2StrobeState++;
            if (this.joy2StrobeState == 24)
            {
                this.joy2StrobeState = 0;
            }

            return ret;
        }

        public virtual void LoadROM()
        {
            if (!this.Nes.Rom.valid || this.Nes.Rom.romCount < 1)
            {
                throw new Exception("NoMapper: Invalid ROM! Unable to load.");
            }

            // Load ROM into memory:
            this.LoadPRGROM();

            // Load CHR-ROM:
            this.LoadCHRROM();

            // Load Battery RAM (if present):
            this.LoadBatteryRam();

            // Reset IRQ:
            //nes.getCpu().doResetInterrupt();
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }

        public void LoadPRGROM()
        {
            if (this.Nes.Rom.romCount > 1)
            {
                // Load the two first banks into memory.
                this.LoadRomBank(0, 0x8000);
                this.LoadRomBank(1, 0xc000);
            }
            else
            {
                // Load the one bank into both memory locations:
                this.LoadRomBank(0, 0x8000);
                this.LoadRomBank(0, 0xc000);
            }
        }

        public void LoadCHRROM()
        {
            // console.log("Loading CHR ROM..");
            if (this.Nes.Rom.vromCount > 0)
            {
                if (this.Nes.Rom.vromCount == 1)
                {
                    this.LoadVromBank(0, 0x0000);
                    this.LoadVromBank(0, 0x1000);
                }
                else
                {
                    this.LoadVromBank(0, 0x0000);
                    this.LoadVromBank(1, 0x1000);
                }
            }
            else
            {
                //System.out.println("There aren't any CHR-ROM banks..");
            }
        }

        public void LoadBatteryRam()
        {
            //if (this.Nes.Rom.batteryRam)
            //{
            //    var ram = this.Nes.Rom.batteryRam;
            //    //utils.copyArrayElements(
            //    //    ram,
            //    //  0,
            //    // this.Nes.Cpu.Mem, 0x6000, 0x2000
            //    //);
            //    // TODO XXX
            //    //if (ram != null && ram.Length == 0x2000)
            //    //{
            //    //    // Load Battery RAM into memory:
            //    //    utils.copyArrayElements(ram, 0, this.Nes.Cpu.Mem, 0x6000, 0x2000);
            //    //}
            //}
        }

        public void LoadRomBank(Int32 bank, Int32 address)
        {
            // Loads a ROM bank into the specified address.
            bank %= this.Nes.Rom.romCount;
            //var data = this.Nes.Rom.Rom[bank];
            //cpuMem.write(address,data,data.length);
            utils.copyArrayElements(
              this.Nes.Rom.rom[bank],
              0,
              this.Nes.Cpu.Mem,
              address,
              16384
            );
        }

        public void LoadVromBank(Int32 bank,Int32 address)
        {
            if (this.Nes.Rom.vromCount == 0)
            {
                return;
            }
            this.Nes.Ppu.triggerRendering();

            utils.copyArrayElements(
              this.Nes.Rom.vrom[bank % this.Nes.Rom.vromCount],
              0,
              this.Nes.Ppu.vramMem,
              address,
              4096
            );

            var vromTile = this.Nes.Rom.vromTile[bank % this.Nes.Rom.vromCount];
            utils.copyArrayElements(
              vromTile,
              0,
              this.Nes.Ppu.ptTile,
              address >> 4,
              256
            );
        }

        public void Load32kRomBank(Int32 bank, Int32 address)
        {
            this.LoadRomBank((bank * 2) % this.Nes.Rom.romCount, address);
            this.LoadRomBank((bank * 2 + 1) % this.Nes.Rom.romCount, address + 16384);
        }

        public void Load8kVromBank(Int32 bank4kStart, Int32 address)
        {
            if (this.Nes.Rom.vromCount == 0)
            {
                return;
            }
            this.Nes.Ppu.triggerRendering();

            this.LoadVromBank(bank4kStart % this.Nes.Rom.vromCount, address);
            this.LoadVromBank(
              (bank4kStart + 1) % this.Nes.Rom.vromCount,
              address + 4096
            );
        }

        public void Load1kVromBank(Int32 bank1k, Int32 address)
        {
            if (this.Nes.Rom.vromCount == 0)
            {
                return;
            }
            this.Nes.Ppu.triggerRendering();

            var bank4k = (Int32)Math.Floor(bank1k / 4.0) % this.Nes.Rom.vromCount;
            var bankoffset = (bank1k % 4) * 1024;
            utils.copyArrayElements(
              this.Nes.Rom.vrom[bank4k],
              bankoffset,
              this.Nes.Ppu.vramMem,
              address,
              1024
            );

            // Update tiles:
            var vromTile = this.Nes.Rom.vromTile[bank4k];
            var baseIndex = address >> 4;
            for (var i = 0; i < 64; i++)
            {
                this.Nes.Ppu.ptTile[baseIndex + i] = vromTile[(bank1k % 4 << 6) + i];
            }
        }

        public void Load2kVromBank(Int32 bank2k, Int32 address)
        {
            if (this.Nes.Rom.vromCount == 0)
            {
                return;
            }
            this.Nes.Ppu.triggerRendering();

            var bank4k = (Int32)Math.Floor(bank2k / 2.0) % this.Nes.Rom.vromCount;
            var bankoffset = (bank2k % 2) * 2048;
            utils.copyArrayElements(
              this.Nes.Rom.vrom[bank4k],
              bankoffset,
              this.Nes.Ppu.vramMem,
              address,
              2048
            );

            // Update tiles:
            var vromTile = this.Nes.Rom.vromTile[bank4k];
            var baseIndex = address >> 4;
            for (var i = 0; i < 128; i++)
            {
                this.Nes.Ppu.ptTile[baseIndex + i] = vromTile[(bank2k % 2 << 7) + i];
            }
        }

        public void Load8kRomBank(Int32 bank8k, Int32 address)
        {
            var bank16k = (Int32)Math.Floor(bank8k / 2.0) % this.Nes.Rom.romCount;
            var offset = (bank8k % 2) * 8192;

            //this.Nes.cpu.Mem.write(address,this.Nes.Rom.Rom[bank16k],offset,8192);
            utils.copyArrayElements(
              this.Nes.Rom.rom[bank16k],
              offset,
              this.Nes.Cpu.Mem,
              address,
              8192
            );
        }

        public virtual void ClockIrqCounter()
        {
            // Does nothing. This is used by the MMC3 mapper.
        }

        // eslint-disable-next-line no-unused-vars
        public static void LatchAccess(Int32 address)
        {
            // Does nothing. This is used by MMC2.
        }

        public Object ToJSON()
        {
            return new JoyState
            {
                joy1StrobeState = this.joy1StrobeState,
                joy2StrobeState = this.joy2StrobeState,
                joypadLastWrite = this.joypadLastWrite,
            };
        }

        public void FromJSON(JoyState s)
        {
            this.joy1StrobeState = s.joy1StrobeState;
            this.joy2StrobeState = s.joy2StrobeState;
            this.joypadLastWrite = s.joypadLastWrite;
        }
    }

    public class Mapper1: Mapper0
    {
        private int regBuffer;
        private int regBufferCounter;
        private int mirroring;
        private int oneScreenMirroring;
        private int prgSwitchingArea;
        private int prgSwitchingSize;
        private int vromSwitchingSize;
        private int romSelectionReg0;
        private int romSelectionReg1;
        private int romBankSelect;

        public Mapper1(Nes nes) : base(nes) { }

        public override void Reset()
        {
            base.Reset();
            this.regBuffer = 0;
            this.regBufferCounter = 0;

            // Register 0:
            this.mirroring = 0;
            this.oneScreenMirroring = 0;
            this.prgSwitchingArea = 1;
            this.prgSwitchingSize = 1;
            this.vromSwitchingSize = 0;

            // Register 1:
            this.romSelectionReg0 = 0;

            // Register 2:
            this.romSelectionReg1 = 0;

            // Register 3:
            this.romBankSelect = 0;
        }

        public override void Write(int address, int value)
        {
            if(address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            // See what should be done with the written value:
            if ((value & 128) != 0)
            {
                // Reset buffering:
                this.regBufferCounter = 0;
                this.regBuffer = 0;

                // Reset register:
                if (GetRegNumber(address) == 0)
                {
                    this.prgSwitchingArea = 1;
                    this.prgSwitchingSize = 1;
                }
            }
            else
            {
                // Continue buffering:
                //regBuffer = (regBuffer & (0xFF-(1<<regBufferCounter))) | ((value & (1<<regBufferCounter))<<regBufferCounter);
                this.regBuffer =
                  (this.regBuffer & (0xff - (1 << this.regBufferCounter))) |
                  ((value & 1) << this.regBufferCounter);
                this.regBufferCounter++;

                if (this.regBufferCounter == 5)
                {
                    // Use the buffered value:
                    this.setReg(GetRegNumber(address), this.regBuffer);

                    // Reset buffer:
                    this.regBuffer = 0;
                    this.regBufferCounter = 0;
                }
            }
        }

        public void setReg(Int32 reg, Int32 value)
        {
            Int32 tmp;
            switch (reg)
            {
                case 0:
                    // Mirroring:
                    tmp = value & 3;
                    if (tmp != this.mirroring)
                    {
                        // Set mirroring:
                        this.mirroring = tmp;
                        if ((this.mirroring & 2) == 0)
                        {
                            // SingleScreen mirroring overrides the other setting:
                            this.Nes.Ppu.setMirroring(Rom.SINGLESCREEN_MIRRORING);
                        }
                        else if ((this.mirroring & 1) != 0)
                        {
                            // Not overridden by SingleScreen mirroring.
                            this.Nes.Ppu.setMirroring(Rom.HORIZONTAL_MIRRORING);
                        }
                        else
                        {
                            this.Nes.Ppu.setMirroring(Rom.VERTICAL_MIRRORING);
                        }
                    }

                    // PRG Switching Area;
                    this.prgSwitchingArea = (value >> 2) & 1;

                    // PRG Switching Size:
                    this.prgSwitchingSize = (value >> 3) & 1;

                    // VROM Switching Size:
                    this.vromSwitchingSize = (value >> 4) & 1;

                    break;

                case 1:
                    // ROM selection:
                    this.romSelectionReg0 = (value >> 4) & 1;

                    // Check whether the cart has VROM:
                    if (this.Nes.Rom.vromCount > 0)
                    {
                        // Select VROM bank at 0x0000:
                        if (this.vromSwitchingSize == 0)
                        {
                            // Swap 8kB VROM:
                            if (this.romSelectionReg0 == 0)
                            {
                                Load8kVromBank(value & 0xf, 0x0000);
                            }
                            else
                            {
                                Load8kVromBank(
                                  (Int32)(Math.Floor(this.Nes.Rom.vromCount / 2.0)) + (value & 0xf),
                                  0x0000
                                );
                            }
                        }
                        else
                        {
                            // Swap 4kB VROM:
                            if (this.romSelectionReg0 == 0)
                            {
                                LoadVromBank(value & 0xf, 0x0000);
                            }
                            else
                            {
                                LoadVromBank(
                                  (Int32)(Math.Floor(this.Nes.Rom.vromCount / 2.0) + (value & 0xf)),
                                  0x0000
                                );
                            }
                        }
                    }

                    break;

                case 2:
                    // ROM selection:
                    this.romSelectionReg1 = (value >> 4) & 1;

                    // Check whether the cart has VROM:
                    if (this.Nes.Rom.vromCount > 0)
                    {
                        // Select VROM bank at 0x1000:
                        if (this.vromSwitchingSize == 1)
                        {
                            // Swap 4kB of VROM:
                            if (this.romSelectionReg1 == 0)
                            {
                                LoadVromBank(value & 0xf, 0x1000);
                            }
                            else
                            {
                                LoadVromBank(
                                  (Int32)(Math.Floor(this.Nes.Rom.vromCount / 2.0) + (value & 0xf)),
                                  0x1000
                                );
                            }
                        }
                    }
                    break;

                default:
                    // Select ROM bank:
                    // -------------------------
                    tmp = value & 0xf;
                    Int32 bank;
                    var baseBank = 0;

                    if (this.Nes.Rom.romCount >= 32)
                    {
                        // 1024 kB cart
                        if (this.vromSwitchingSize == 0)
                        {
                            if (this.romSelectionReg0 == 1)
                            {
                                baseBank = 16;
                            }
                        }
                        else
                        {
                            baseBank =
                              (this.romSelectionReg0 | (this.romSelectionReg1 << 1)) << 3;
                        }
                    }
                    else if (this.Nes.Rom.romCount >= 16)
                    {
                        // 512 kB cart
                        if (this.romSelectionReg0 == 1)
                        {
                            baseBank = 8;
                        }
                    }

                    if (this.prgSwitchingSize == 0)
                    {
                        // 32kB
                        bank = baseBank + (value & 0xf);
                        Load32kRomBank(bank, 0x8000);
                    }
                    else
                    {
                        // 16kB
                        bank = baseBank * 2 + (value & 0xf);
                        if (this.prgSwitchingArea == 0)
                        {
                            LoadRomBank(bank, 0xc000);
                        }
                        else
                        {
                            LoadRomBank(bank, 0x8000);
                        }
                    }
                    break;
            }
        }

        public static Int32 GetRegNumber(Int32 address)
        {
            if (address >= 0x8000 && address <= 0x9fff)
            {
                return 0;
            }
            else if (address >= 0xa000 && address <= 0xbfff)
            {
                return 1;
            }
            else if (address >= 0xc000 && address <= 0xdfff)
            {
                return 2;
            }
            else
            {
                return 3;
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("MMC1: Invalid ROM! Unable to load.");
            }
            // Load PRG-ROM:
            LoadRomBank(0, 0x8000); //   First ROM bank..
            LoadRomBank(this.Nes.Rom.romCount - 1, 0xc000); // ..and last ROM bank.

            // Load CHR-ROM:
            LoadCHRROM();

            // Load Battery RAM (if present):
            LoadBatteryRam();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }

        public static void switchLowHighPrgRom() { }
        public static void switch16to32() { }
        public static void switch32to16() { }
    }

    public class Mapper2 : Mapper0
    {
        public Mapper2(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // This is a ROM bank select command.
                // Swap in the given ROM bank at 0x8000:
                LoadRomBank(value, 0x8000);
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("UNROM: Invalid ROM! Unable to load.");
            }

            // Load PRG-ROM:
            LoadRomBank(0, 0x8000);
            LoadRomBank(this.Nes.Rom.romCount - 1, 0xc000);

            // Load CHR-ROM:
            LoadCHRROM();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }
    }

    public class Mapper3 : Mapper0
    {
        public Mapper3(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // This is a ROM bank select command.
                // Swap in the given ROM bank at 0x8000:
                // This is a VROM bank select command.
                // Swap in the given VROM bank at 0x0000:
                var bank = (value % (this.Nes.Rom.vromCount / 2)) * 2;
                LoadVromBank(bank, 0x0000);
                LoadVromBank(bank + 1, 0x1000);
                Load8kVromBank(value * 2, 0x0000);
            }
        }
    }

    public class Mapper4 : Mapper0
    {
        private bool prgAddressChanged;
        private int command;
        private int prgAddressSelect;
        private int chrAddressSelect;
        private int irqCounter;
        private int irqLatchValue;
        private int irqEnable;

        public const int CMD_SEL_2_1K_VROM_0000 = 0;
        public const int CMD_SEL_2_1K_VROM_0800 = 1;
        public const int CMD_SEL_1K_VROM_1000 = 2;
        public const int CMD_SEL_1K_VROM_1400 = 3;
        public const int CMD_SEL_1K_VROM_1800 = 4;
        public const int CMD_SEL_1K_VROM_1C00 = 5;
        public const int CMD_SEL_ROM_PAGE1 = 6;
        public const int CMD_SEL_ROM_PAGE2 = 7;

        public Mapper4(Nes nes):base(nes)
        {

            //this.CMD_SEL_2_1K_VROM_0000 = 0;
            //this.CMD_SEL_2_1K_VROM_0800 = 1;
            //this.CMD_SEL_1K_VROM_1000 = 2;
            //this.CMD_SEL_1K_VROM_1400 = 3;
            //this.CMD_SEL_1K_VROM_1800 = 4;
            //this.CMD_SEL_1K_VROM_1C00 = 5;
            //this.CMD_SEL_ROM_PAGE1 = 6;
            //this.CMD_SEL_ROM_PAGE2 = 7;
            //this.command = null;
            //this.prgAddressSelect = null;
            //this.chrAddressSelect = null;
            // this.pageNumber = null;
            //this.irqCounter = null;
            //this.irqLatchValue = null;
            //this.irqEnable = null;
            this.prgAddressChanged = false;
        }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }

            switch (address)
            {
                case 0x8000:
                    // Command/Address Select register
                    this.command = value & 7;
                    var tmp = (value >> 6) & 1;
                    if (tmp != this.prgAddressSelect)
                    {
                        this.prgAddressChanged = true;
                    }
                    this.prgAddressSelect = tmp;
                    this.chrAddressSelect = (value >> 7) & 1;
                    break;

                case 0x8001:
                    // Page number for command
                    this.executeCommand(this.command, value);
                    break;

                case 0xa000:
                    // Mirroring select
                    if ((value & 1) != 0)
                    {
                        this.Nes.Ppu.setMirroring(Rom.HORIZONTAL_MIRRORING);
                    }
                    else
                    {
                        this.Nes.Ppu.setMirroring(Rom.VERTICAL_MIRRORING);
                    }
                    break;

                case 0xa001:
                    // SaveRAM Toggle
                    // TODO
                    //nes.getRom().setSaveState((value&1)!=0);
                    break;

                case 0xc000:
                    // IRQ Counter register
                    this.irqCounter = value;
                    //nes.ppu.mapperIrqCounter = 0;
                    break;

                case 0xc001:
                    // IRQ Latch register
                    this.irqLatchValue = value;
                    break;

                case 0xe000:
                    // IRQ Control Reg 0 (disable)
                    //irqCounter = irqLatchValue;
                    this.irqEnable = 0;
                    break;

                case 0xe001:
                    // IRQ Control Reg 1 (enable)
                    this.irqEnable = 1;
                    break;

                default:
                    // Not a MMC3 register.
                    // The game has probably crashed,
                    // since it tries to write to ROM..
                    // IGNORE.
                    break;
            }
        }

        public void executeCommand(Int32 cmd, Int32 arg) 
        {
            switch (cmd)
            {
                case CMD_SEL_2_1K_VROM_0000:
                    // Select 2 1KB VROM pages at 0x0000:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x0000);
                        Load1kVromBank(arg + 1, 0x0400);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x1000);
                        Load1kVromBank(arg + 1, 0x1400);
                    }
                    break;

                case CMD_SEL_2_1K_VROM_0800:
                    // Select 2 1KB VROM pages at 0x0800:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x0800);
                        Load1kVromBank(arg + 1, 0x0c00);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x1800);
                        Load1kVromBank(arg + 1, 0x1c00);
                    }
                    break;

                case CMD_SEL_1K_VROM_1000:
                    // Select 1K VROM Page at 0x1000:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x1000);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x0000);
                    }
                    break;

                case CMD_SEL_1K_VROM_1400:
                    // Select 1K VROM Page at 0x1400:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x1400);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x0400);
                    }
                    break;

                case CMD_SEL_1K_VROM_1800:
                    // Select 1K VROM Page at 0x1800:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x1800);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x0800);
                    }
                    break;

                case CMD_SEL_1K_VROM_1C00:
                    // Select 1K VROM Page at 0x1C00:
                    if (this.chrAddressSelect == 0)
                    {
                        Load1kVromBank(arg, 0x1c00);
                    }
                    else
                    {
                        Load1kVromBank(arg, 0x0c00);
                    }
                    break;

                case CMD_SEL_ROM_PAGE1:
                    if (this.prgAddressChanged)
                    {
                        // Load the two hardwired banks:
                        if (this.prgAddressSelect == 0)
                        {
                            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2, 0xc000);
                        }
                        else
                        {
                            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2, 0x8000);
                        }
                        this.prgAddressChanged = false;
                    }

                    // Select first switchable ROM page:
                    if (this.prgAddressSelect == 0)
                    {
                        Load8kRomBank(arg, 0x8000);
                    }
                    else
                    {
                        Load8kRomBank(arg, 0xc000);
                    }
                    break;

                case CMD_SEL_ROM_PAGE2:
                    // Select second switchable ROM page:
                    Load8kRomBank(arg, 0xa000);

                    // hardwire appropriate bank:
                    if (this.prgAddressChanged)
                    {
                        // Load the two hardwired banks:
                        if (this.prgAddressSelect == 0)
                        {
                            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2, 0xc000);
                        }
                        else
                        {
                            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2, 0x8000);
                        }
                        this.prgAddressChanged = false;
                    }
                    break;
            }
        }

        public override void LoadROM() 
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("MMC3: Invalid ROM! Unable to load.");
            }

            // Load hardwired PRG banks (0xC000 and 0xE000):
            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2, 0xc000);
            Load8kRomBank((this.Nes.Rom.romCount - 1) * 2 + 1, 0xe000);

            // Load swappable PRG banks (0x8000 and 0xA000):
            Load8kRomBank(0, 0x8000);
            Load8kRomBank(1, 0xa000);

            // Load CHR-ROM:
            LoadCHRROM();

            // Load Battery RAM (if present):
            LoadBatteryRam();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }

        public override void ClockIrqCounter()
        {
            if (this.irqEnable == 1)
            {
                this.irqCounter--;
                if (this.irqCounter < 0)
                {
                    // Trigger IRQ:
                    //nes.getCpu().doIrq();
                    this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_NORMAL);
                    this.irqCounter = this.irqLatchValue;
                }
            }
        }
    }

    public class Mapper5 : Mapper0
    {
        private int prg_size;
        private int sram_we_b;
        private int graphic_mode;
        private int nametable_mode;
        private Int32[] nametable_type = new Int32[5];
        private int fill_chr;
        private int fill_pal;
        private int chr_mode;
        private Int32[][] chr_page;
        private int sram_we_a;
        private int mult_b;
        private int mult_a;
        private int irq_enable;
        private int irq_line;
        private int split_page;
        private int split_scroll;
        private int split_control;
        public int irq_status;
        private int chr_size;

        public Mapper5(Nes nes) : base(nes) { }

        //public void write(int address, int value)
        //{
        //    // Writes to addresses other than MMC registers are handled by NoMapper.
        //    if (address < 0x8000)
        //    {
        //        base.Write(address, value);
        //    }
        //    else
        //    {
        //        Load8kVromBank(value, 0x0000);
        //    }
        //}
        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x5000)
            {
                base.Write(address, value);
                return;
            }

            switch (address)
            {
                case 0x5100:
                    this.prg_size = value & 3;
                    break;
                case 0x5101:
                    this.chr_size = value & 3;
                    break;
                case 0x5102:
                    this.sram_we_a = value & 3;
                    break;
                case 0x5103:
                    this.sram_we_b = value & 3;
                    break;
                case 0x5104:
                    this.graphic_mode = value & 3;
                    break;
                case 0x5105:
                    this.nametable_mode = value;
                    this.nametable_type[0] = value & 3;
                    Load1kVromBank(value & 3, 0x2000);
                    value >>= 2;
                    this.nametable_type[1] = value & 3;
                    Load1kVromBank(value & 3, 0x2400);
                    value >>= 2;
                    this.nametable_type[2] = value & 3;
                    Load1kVromBank(value & 3, 0x2800);
                    value >>= 2;
                    this.nametable_type[3] = value & 3;
                    Load1kVromBank(value & 3, 0x2c00);
                    break;
                case 0x5106:
                    this.fill_chr = value;
                    break;
                case 0x5107:
                    this.fill_pal = value & 3;
                    break;
                case 0x5113:
                    // TODO XXX
                    // this.SetBank_SRAM(3, value & 3);
                    break;
                case 0x5114:
                case 0x5115:
                case 0x5116:
                case 0x5117:
                    // TODO XXX
                    // this.SetBank_CPU(address, value);
                    break;
                case 0x5120:
                case 0x5121:
                case 0x5122:
                case 0x5123:
                case 0x5124:
                case 0x5125:
                case 0x5126:
                case 0x5127:
                    this.chr_mode = 0;
                    this.chr_page[0][address & 7] = value;
                    // TODO XXX
                    // this.SetBank_PPU();
                    break;
                case 0x5128:
                case 0x5129:
                case 0x512a:
                case 0x512b:
                    this.chr_mode = 1;
                    this.chr_page[1][(address & 3) + 0] = value;
                    this.chr_page[1][(address & 3) + 4] = value;
                    // TODO XXX
                    // this.SetBank_PPU();
                    break;
                case 0x5200:
                    this.split_control = value;
                    break;
                case 0x5201:
                    this.split_scroll = value;
                    break;
                case 0x5202:
                    this.split_page = value & 0x3f;
                    break;
                case 0x5203:
                    this.irq_line = value;
                    // TODO XXX
                    //this.Nes.Cpu.ClearIRQ();
                    break;
                case 0x5204:
                    this.irq_enable = value;
                    // TODO XXX
                    //this.Nes.Cpu.ClearIRQ();
                    break;
                case 0x5205:
                    this.mult_a = value;
                    break;
                case 0x5206:
                    this.mult_b = value;
                    break;
                default:
                    if (address >= 0x5000 && address <= 0x5015)
                    {
                        // this.Nes.Papu.exWrite(address, value);
                    }
                    else if (address >= 0x5c00 && address <= 0x5fff)
                    {
                        if (this.graphic_mode == 2)
                        {
                            // ExRAM
                            // vram write
                        }
                        else if (this.graphic_mode != 3)
                        {
                            // Split,ExGraphic
                            if ((this.irq_status & 0x40) > 0)
                            {
                                // vram write
                            }
                            else
                            {
                                // vram write
                            }
                        }
                    }
                    else if (address >= 0x6000 && address <= 0x7fff)
                    {
                        if (this.sram_we_a == 2 && this.sram_we_b == 1)
                        {
                            // additional ram write
                        }
                    }
                    break;
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("UNROM: Invalid ROM! Unable to load.");
            }

            // Load PRG-ROM:
            Load8kRomBank(this.Nes.Rom.romCount * 2 - 1, 0x8000);
            Load8kRomBank(this.Nes.Rom.romCount * 2 - 1, 0xa000);
            Load8kRomBank(this.Nes.Rom.romCount * 2 - 1, 0xc000);
            Load8kRomBank(this.Nes.Rom.romCount * 2 - 1, 0xe000);

            // Load CHR-ROM:
            LoadCHRROM();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }
    }

    public class Mapper7: Mapper0
    {
        public Mapper7(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
            }
            else
            {
                Load32kRomBank(value & 0x7, 0x8000);
                if ((value & 0x10) != 0)
                {
                    this.Nes.Ppu.setMirroring(Rom.SINGLESCREEN_MIRRORING2);
                }
                else
                {
                    this.Nes.Ppu.setMirroring(Rom.SINGLESCREEN_MIRRORING);
                }
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("AOROM: Invalid ROM! Unable to load.");
            }

            // Load PRG-ROM:
            LoadPRGROM();

            // Load CHR-ROM:
            LoadCHRROM();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }
    }

    public class Mapper11 : Mapper0
    {
        public Mapper11(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // Swap in the given PRG-ROM bank:
                var prgbank1 = ((value & 0xf) * 2) % this.Nes.Rom.romCount;
                var prgbank2 = ((value & 0xf) * 2 + 1) % this.Nes.Rom.romCount;

                LoadRomBank(prgbank1, 0x8000);
                LoadRomBank(prgbank2, 0xc000);

                if (this.Nes.Rom.vromCount > 0)
                {
                    // Swap in the given VROM bank at 0x0000:
                    var bank = ((value >> 4) * 2) % this.Nes.Rom.vromCount;
                    LoadVromBank(bank, 0x0000);
                    LoadVromBank(bank + 1, 0x1000);
                }
            }
        }
    }

    public class Mapper34 : Mapper0
    {
        public Mapper34(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                Load32kRomBank(value, 0x8000);
            }
        }
    }

    public class Mapper38 : Mapper0
    {
        public Mapper38(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            if (address < 0x7000 || address > 0x7fff)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // Swap in the given PRG-ROM bank at 0x8000:
                Load32kRomBank(value & 3, 0x8000);

                // Swap in the given VROM bank at 0x0000:
                Load8kVromBank(((value >> 2) & 3) * 2, 0x0000);
            }
        }
    }

    public class Mapper66 : Mapper0
    {
        public Mapper66(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // Swap in the given PRG-ROM bank at 0x8000:
                Load32kRomBank((value >> 4) & 3, 0x8000);

                // Swap in the given VROM bank at 0x0000:
                Load8kVromBank((value & 3) * 2, 0x0000);
            }
        }
    }

    public class Mapper94 : Mapper0
    {
        public Mapper94(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // This is a ROM bank select command.
                // Swap in the given ROM bank at 0x8000:
                LoadRomBank(value >> 2, 0x8000);
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("UN1ROM: Invalid ROM! Unable to load.");
            }

            // Load PRG-ROM:
            LoadRomBank(0, 0x8000);
            LoadRomBank(this.Nes.Rom.romCount - 1, 0xc000);

            // Load CHR-ROM:
            LoadCHRROM();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }
    }

    public class Mapper140 : Mapper0
    {
        public Mapper140(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            if (address < 0x6000 || address > 0x7fff)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // Swap in the given PRG-ROM bank at 0x8000:
                Load32kRomBank((value >> 4) & 3, 0x8000);

                // Swap in the given VROM bank at 0x0000:
                Load8kVromBank((value & 0xf) * 2, 0x0000);
            }
        }
    }

    public class Mapper180 : Mapper0
    {
        public Mapper180(Nes nes) : base(nes) { }

        public override void Write(int address, int value)
        {
            // Writes to addresses other than MMC registers are handled by NoMapper.
            if (address < 0x8000)
            {
                base.Write(address, value);
                return;
            }
            else
            {
                // This is a ROM bank select command.
                // Swap in the given ROM bank at 0xc000:
                LoadRomBank(value, 0xc000);
            }
        }

        public override void LoadROM()
        {
            if (!this.Nes.Rom.valid)
            {
                throw new Exception("Mapper 180: Invalid ROM! Unable to load.");
            }

            // Load PRG-ROM:
            LoadRomBank(0, 0x8000);
            LoadRomBank(this.Nes.Rom.romCount - 1, 0xc000);

            // Load CHR-ROM:
            LoadCHRROM();

            // Do Reset-Interrupt:
            this.Nes.Cpu.RequestIrq(this.Nes.Cpu.IRQ_RESET);
        }
    }

    public class JoyState
    {
        internal int joy1StrobeState;
        internal int joy2StrobeState;
        internal int joypadLastWrite;
    }
}
